﻿Namespace ServerScripting

#Region "Scripting"

    Public Class ServerScript

        Private Enum ScriptLineType
            FunctionHeader = 0
            CallCommand = 1
            Logic = 3
            Text = 4
        End Enum

        Private Structure ScriptLine

            Dim sType As ScriptLineType
            Dim sCommand As String
            Dim aArgs As List(Of String)

        End Structure

        Public mFileName As String
        Private aScript As List(Of ScriptLine)

        Public Function GetArg(str As String, conn As Connection) As String

            If Left(str, 1) = "$" Then
                Return conn.GetValue(Right(str, str.Length - 1))
            End If

            Return str

        End Function

        Public Function RunCommand(command As String, conn As Connection) As Integer
            Dim line As ScriptLine
            Dim InFunction As Boolean

            Dim DoingLogic As Boolean
            Dim LogicBool As Boolean

            For i As Integer = 0 To aScript.Count - 1

                'load the current line
                line = aScript.Item(i)

                'check if we are inside the function
                If InFunction Then

                    'if DoingLogic flag is set we are processing logic, if this is a logic line it needs checking to make sure it is to be trigged by the IF statement result
                    If DoingLogic And line.sType = ScriptLineType.Logic Then

                        If UCase(line.sCommand) = "IF" Then
                            GoTo ProcessIf
                        End If

                        If LogicBool = True Then

                            'check TRUE logic result

                            If UCase(line.sCommand) = "ACT" Then
                                'carry on processing statements
                                GoTo ProcessLogic
                            ElseIf UCase(line.sCommand) = "SAY" Then
                                'TODO: #SAY
                                Dim tmpStr As String = ""

                                Do

                                    i += 1
                                    'make sure we dont go beyond the end of the file
                                    If i = aScript.Count Then Return 0
                                    'load the next line
                                    line = aScript(i)

                                    If line.sType = ScriptLineType.Text Then

                                        tmpStr &= line.sCommand & vbCrLf

                                    Else

                                        i -= 1
                                        Exit Do

                                    End If

                                Loop

                                If tmpStr.Length > 0 Then Say(tmpStr, conn)

                            Else
                                'the opposite logic result statements have been reached, return
                                Return 1
                            End If

                        Else

                            'check FALSE logic result

                            If UCase(line.sCommand) = "ELSEACT" Then
                                'carry on processing statements
                                GoTo ProcessLogic
                            ElseIf UCase(line.sCommand) = "ELSESAY" Then

                                'TODO: #ELSESAY
                                Dim tmpStr As String = ""

                                Do

                                    i += 1
                                    'make sure we dont go beyond the end of the file
                                    If i = aScript.Count Then Return 0
                                    'load the next line
                                    line = aScript(i)

                                    If line.sType = ScriptLineType.Text Then

                                        tmpStr &= line.sCommand & vbCrLf

                                    Else

                                        i -= 1
                                        Exit Do

                                    End If

                                Loop

                                If tmpStr.Length > 0 Then Say(tmpStr, conn)

                            Else

                                'the opposite logic result statements have been reached, return
                                Return 1
                            End If

                        End If




                        'is this a command call?
                    ElseIf line.sType = ScriptLineType.CallCommand Then

                        'indicates the end of a script command
                        If LCase(line.sCommand) = "break;" Then

                            'end script
                            Return 1


                            'scripted command call, usage:
                            'for an internal call
                            '@[name of command within same file]
                            'OR, for an external call
                            '@[scriptfilename.txt]@[name of command within scriptfilename.txt]

                        Else

                            ProcessCall(line, conn)

                        End If




                        'logic line
                    ElseIf line.sType = ScriptLineType.Logic Then

                        'IF statements
                        If UCase(line.sCommand) = "IF" Then

ProcessIf:

                            'set the flag to indicate the script is processing a logic statement
                            DoingLogic = True
                            LogicBool = False

                            'iterate through the conditions
                            Do


                                'load the next statement if the IF, making sure not to go past the end of the file
                                i += 1
                                If i = aScript.Count Then Exit Function
                                line = aScript.Item(i)

                                'all lines in the statement will show as CallCommand's
                                If line.sType = ScriptLineType.CallCommand Then

                                    'check the players gender against the given gender
                                    'eg 'CHECKSEX M'    or    'CHECKSEX F'
                                    If line.sCommand = "CHECKSEX" Then

                                        If LCase(line.aArgs(0)) = "m" Then

                                            If conn.PlayerInfo.BYTE_GENDER = Gender.Male Then
                                                'statement was true
                                                LogicBool = True
                                            Else
                                                'statement was false, can now skip the rest of the lines
                                                LogicBool = False
                                                Exit Do
                                            End If

                                        ElseIf LCase(line.aArgs(0)) = "f" Then

                                            If conn.PlayerInfo.BYTE_GENDER = Gender.Female Then
                                                'statement was true
                                                LogicBool = True

                                            Else
                                                'statement was false, can now skip the rest of the lines
                                                LogicBool = False
                                                Exit Do
                                            End If

                                        End If

                                        'check the players job, accepts the values; Warrior:='WAR', Sorcerer='WIZ'/'SOR', Taoist='TAO'
                                        'eg: CHECKJOB WAR
                                    ElseIf line.sCommand = "CHECKJOB" Then


                                        'determine the Job we are checking for
                                        Dim chkJob As CharacterJobs
                                        Select Case UCase(line.aArgs(0))

                                            Case "WAR"
                                                chkJob = CharacterJobs.Warrior

                                            Case "WIZ", "SOR"
                                                chkJob = CharacterJobs.Sorcerer

                                            Case "TAO"
                                                chkJob = CharacterJobs.Taoist

                                        End Select


                                        'set the LogicBool flag depending on whether the player IS of this Job/Class (True) or not (False)
                                        If conn.PlayerJob = chkJob Then
                                            LogicBool = True
                                        Else
                                            'statement was false, can now skip the rest of the lines
                                            LogicBool = False
                                            Exit Do
                                        End If

                                    End If

                                    'a logic line will indicate the end of the IF statements
                                ElseIf line.sType = ScriptLineType.Logic Then

                                    Exit Do

                                End If


                            Loop

                            'LogicBool now holds the result of the IF statements, this will determine wheter to run the ACT/SAY or ELSEACT/ELSESAY statemnts

                            Do

                                'most of these will need to be skipped as they will be part of an ACT/SAY statement that has not been triggered
                                If line.sType = ScriptLineType.CallCommand Then


                                    'we have reached the end of the function, there was no corresponding ACT/SAY for the LogicBool result
                                    If LCase(line.sCommand) = "break;" Then
                                        'exit the script
                                        Return 1
                                    End If


                                    'logic line
                                ElseIf line.sType = ScriptLineType.Logic Then

                                    If UCase(line.sCommand) = "IF" Then GoTo ProcessIf

                                    'if the logic was true
                                    If LogicBool = True Then
                                        'check for #ACT
                                        If UCase(line.sCommand) = "ACT" Then
                                            'process the commans withing the logic statement, DoingLogic flag is already set to true so it will only continue processing at a #SAY statement
                                            GoTo ProcessLogic
                                        End If

                                    Else
                                        'logic returned false

                                        'check for #ELSEACT
                                        If UCase(line.sCommand) = "ELSEACT" Then

                                            'process the commands within logic statement
                                            GoTo ProcessLogic

                                        End If

                                    End If

                                End If

                                i += 1
                                'make sure we dont go beyond the end of the file
                                If i = aScript.Count Then Return 0
                                'load the next line
                                line = aScript(i)

                            Loop

                        ElseIf UCase(line.sCommand) = "SAY" And Not DoingLogic Then

                            Dim tmpStr As String = ""

                            'TODO: Non IF-bound #SAY
                            Do

                                i += 1
                                'make sure we dont go beyond the end of the file
                                If i = aScript.Count Then Return 0
                                'load the next line
                                line = aScript(i)

                                If line.sType = ScriptLineType.Text Then

                                    tmpStr &= line.sCommand & vbCrLf

                                Else

                                    i -= 1
                                    Exit Do

                                End If

                            Loop

                            If tmpStr.Length > 0 Then Say(tmpStr, conn)

                        End If

                    End If

                Else


                    'check for a function header
                    If line.sType = ScriptLineType.FunctionHeader Then
                        'check the name of the function against the one we are calling
                        If line.sCommand = command Then

                            'this is the function header of the function we are calling, set the InFunction flag to true
                            InFunction = True

                        End If
                    End If


                End If



                'label for jumping to next line and continuing processing of calls
ProcessLogic:



            Next

        End Function

        Private Sub ProcessCall(line As ScriptLine, conn As Connection)

            If Left(line.sCommand, 1) = "@" Then

                'line contains more than one @ symbol, this means an external call
                If InStr(2, line.sCommand, "@") > 0 Then

                    'seperate command name and file name from the line
                    Dim tmpCmd As String = Right(line.sCommand, line.sCommand.Length - (InStr(2, line.sCommand, "@") - 1))
                    Dim tmpFile As String = Mid(line.sCommand, 2, InStr(2, line.sCommand, "@") - 2)

                    'call the command through the main scripting class
                    ServerScripting.Scripts.RunCommand(tmpFile, tmpCmd, conn)

                Else

                    'call the internal command
                    RunCommand(line.sCommand, conn)

                End If



                'creates a new item and adds it to the players bag
                'usage: CREATEANDGIVE ["Item Name"] [#Count]

                            ElseIf LCase(line.sCommand) = "createandgive" Then

                Dim id As Long

                If DoesItemStack(line.aArgs(0)) Then
                    'Create the item
                    id = CreateNewItem(line.aArgs(0), conn.CharacterID, line.aArgs(1))
                    'load the item into the players bag
                    LoadNewItem(conn, id)
                Else
                    For iT As Integer = 1 To line.aArgs(1)
                        'Create the item
                        id = CreateNewItem(line.aArgs(0), conn.CharacterID)
                        'load the item into the players bag
                        LoadNewItem(conn, id)
                    Next
                End If

                'Retrives a column from TBL_ITEMBASE
                'Arguments:
                'ID_ITEMBASE, Column Name, Destination Var Name
            ElseIf UCase(line.sCommand) = "GETITEMBASECOL" Then

                'select the column based on given id_itembase
                Dim sSql As String = "SELECT a." & GetArg(line.aArgs(1), conn) & " FROM TBL_ITEMBASE a JOIN TBL_ITEMS b ON b.ID_ITEMBASE = a.ID_ITEMBASE WHERE b.ID_ITEM=" & GetArg(line.aArgs(0), conn) & ";"
                Dim row As Collection = DB.GameDB.ExecuteQueryFirstRecord(sSql)

                'check for valid result
                If Not row Is Nothing Then
                    If row.Count > 0 Then
                        'set the destination var to the column value
                        conn.SetValue(line.aArgs(2), GetArg(row(line.aArgs(1)), conn))
                    End If
                End If

            ElseIf UCase(line.sCommand) = "GIVEHP" Then

                conn.VitalPoints.HP += GetArg(line.aArgs(0), conn)
                If conn.VitalPoints.HP > conn.VitalPoints.MAXHP Then conn.VitalPoints.HP = conn.VitalPoints.MAXHP

            ElseIf UCase(line.sCommand) = "GIVEMP" Then

                conn.VitalPoints.MP += GetArg(line.aArgs(0), conn)
                If conn.VitalPoints.MP > conn.VitalPoints.MAXMP Then conn.VitalPoints.MP = conn.VitalPoints.MAXMP

            ElseIf UCase(line.sCommand) = "UPDATEVITALS" Then

                If line.aArgs.Count > 0 Then
                    If UCase(line.aArgs(0)) = "TRUE" Then
                        UpdatePlayerHealth(conn, True)
                    Else
                        UpdatePlayerHealth(conn)
                    End If
                Else
                    UpdatePlayerHealth(conn)
                End If

            ElseIf UCase(line.sCommand) = "TELEPORT" Then

                Dim pi As PositionInfo
                pi.INT2_X = GetArg(line.aArgs(1), conn)
                pi.INT2_Y = GetArg(line.aArgs(2), conn)
                pi.STR_MAP = GetArg(line.aArgs(0), conn)

                'teleport the GM
                TeleportPlayer(conn, pi)

            ElseIf UCase(line.sCommand) = "LOADNPCSHOPBUY" Then
                Dim map As MirMap = GetMapFromConn(conn)
                map.NPCs.LoadShopBuy(conn.GetValue(Constants.SysVarName_NPCID), conn)

            ElseIf UCase(line.sCommand) = "LOADNPCSHOPSELL" Then

                Dim map As MirMap = GetMapFromConn(conn)
                If map.NPCs.lstShops.ContainsKey(conn.GetValue(Constants.SysVarName_NPCID)) Then
                    GetMapFromConn(conn).NPCs.lstShops(conn.GetValue(Constants.SysVarName_NPCID)).SendSellInfo(conn)
                End If

            ElseIf UCase(line.sCommand) = "LOADNPCREPAIR" Then

                Dim map As MirMap = GetMapFromConn(conn)
                If map.NPCs.lstShops.ContainsKey(conn.GetValue(Constants.SysVarName_NPCID)) Then
                    GetMapFromConn(conn).NPCs.lstShops(conn.GetValue(Constants.SysVarName_NPCID)).SendRepairInfo(conn)
                End If

            ElseIf UCase(line.sCommand) = "TOWNTELEPORT" Then

                HomeMaps.DoTownTeleport(conn)

            ElseIf UCase(line.sCommand) = "RANDOMTELEPORT" Then
                RandomTeleport(conn)


            End If


        End Sub

        Public Sub New(filename As String)

            mFileName = filename

            Dim stream As System.IO.StreamReader = FileIO.FileSystem.OpenTextFileReader(filename)

            aScript = New List(Of ScriptLine)

            Do Until stream.EndOfStream

                Try

                    ParseLine(stream.ReadLine())

                Catch ex As Exception

                    Throw New Exception(ex.Message)

                End Try
            Loop

        End Sub

        Private Sub ParseLine(line As String)

            If line.Length = 0 Then Exit Sub

            TrimLineTabs(line)

            Dim tmpScriptLine As New ScriptLine
            Static bIsSay As Boolean

            Select Case Left(line, 1)

                Case ";"
                    'line is a comment
                    Exit Sub

                Case "["
                    'line is a function header
                    tmpScriptLine.sType = ScriptLineType.FunctionHeader
                    'strip leading '['
                    line = Right(line, line.Length - 1)
                    'strip following ']'
                    line = Left(line, line.Length - 1)
                    'set command string to the remaining string (the function name)
                    tmpScriptLine.sCommand = line

                Case "#"
                    'line is logic (#IF, #ACT, #ELSEACT, #SAY, #ELSESAY)
                    tmpScriptLine.sType = ScriptLineType.Logic
                    'strip line of '#'
                    line = Right(line, line.Length - 1)

                    'get position of first space in line (if any)
                    Dim p As Integer = InStr(line, " ")

                    If p = 0 Then
                        'position being 0 means no spaces, means no args to put into list
                        tmpScriptLine.sCommand = line
                        'say commands need to set bIsSay flag as following lines are not commands and args
                        If tmpScriptLine.sCommand = "SAY" Or tmpScriptLine.sCommand = "ELSESAY" Then
                            bIsSay = True
                        Else
                            bIsSay = False
                        End If
                    Else
                        'space means args need to be put into list
                        'put everything before space (the command/function to call) into command string
                        tmpScriptLine.sCommand = Left(line, p - 1)
                        'say commands need to set bIsSay flag as following lines are not commands and args
                        If tmpScriptLine.sCommand = "SAY" Or tmpScriptLine.sCommand = "ELSESAY" Then
                            bIsSay = True
                        Else
                            bIsSay = False
                        End If
                        'strip logic name from line
                        line = Right(line, line.Length - p)
                        'parse args from line, put them into ScriptLine structure
                        tmpScriptLine.aArgs = ParseArgs(line)
                    End If

                Case Else

                    If bIsSay Then
                        'current logic is #SAY or #ELSESAY

                        If LCase(line) = "break;" Then
                            'break command
                            tmpScriptLine.sType = ScriptLineType.CallCommand
                        Else

                            'line is text for say type
                            tmpScriptLine.sType = ScriptLineType.Text

                        End If

                        'put text into command string
                        tmpScriptLine.sCommand = line

                    Else

                        'line is command call
                        tmpScriptLine.sType = ScriptLineType.CallCommand

                        'get position of first space in line (if any)
                        Dim p As Integer = InStr(line, " ")

                        If p = 0 Then
                            'position being 0 means no spaces, means no args to put into list
                            tmpScriptLine.sCommand = line
                            tmpScriptLine.aArgs = New List(Of String)
                        Else
                            'space means args need to be put into list
                            'put everything before space (the command/function to call) into command string
                            tmpScriptLine.sCommand = Left(line, p - 1)
                            'strip command/function name from line
                            line = Right(line, line.Length - p)
                            'parse args from line, put them into ScriptLine structure
                            tmpScriptLine.aArgs = ParseArgs(line)
                        End If

                    End If

            End Select

            aScript.Add(tmpScriptLine)

        End Sub

        Private Function ParseArgs(strArgs As String) As List(Of String)

            Dim args As New List(Of String)
            Dim p As Integer
            Dim tmpStr As String

            Do

                If strArgs.Length = 0 Then Exit Do

                If Left(strArgs, 1) = Chr(34) Then
                    p = InStr(2, strArgs, Chr(34))
                    If p = 0 Then Exit Do
                    tmpStr = Mid(strArgs, 2, p - 2)
                    args.Add(tmpStr)
                    strArgs = Right(strArgs, strArgs.Length - p)
                Else
                    'get position of first space in argument string
                    p = InStr(strArgs, " ")

                    If p = 0 Then
                        'position of space being 0 means only one argument left in string
                        'add last/only argument to list and exit do loop
                        args.Add(strArgs)
                        Exit Do

                    Else

                        'if position was 1, this would mean the space was at the beginning of the string, therefore no argument to take out before this position
                        If p <> 1 Then

                            'store argument in temp string
                            tmpStr = Left(strArgs, p - 1)
                            'add argument to list
                            args.Add(tmpStr)

                        End If

                        'strip this argument and space from beginning of argument string
                        strArgs = Right(strArgs, strArgs.Length - p)

                    End If

                End If

            Loop

            Return args

        End Function

        Shared Sub Say(Text As String, conn As Connection)

            Dim struct As SayInfo

            struct.Text = Text

            Dim pkt As Packet
            pkt.PacketType = GamePacketType.NPCSay
            pkt.PacketOpCode = PacketOpCode.Notify
            pkt.Username = conn.Username

            Socket.SockServer.SendData(Protocol.MakePacket(pkt, struct, conn.Crypto), conn.Index)

        End Sub

    End Class

    Friend Class Scripts

        Private Shared aServerScripts As List(Of ServerScript)

        Public Shared Function LoadScripts() As Integer

            Output.AddLine("\B0\F9SS>\F0\B8 Loading scripts...")

            'initialize scripts list
            aServerScripts = New List(Of ServerScript)

            'temporary variable for holding current script
            Dim tmpScript As ServerScript

            'get collection of script files
            Dim files As Collections.ObjectModel.ReadOnlyCollection(Of String) = FileIO.FileSystem.GetFiles(Globals.sServerPath & "scripts\", FileIO.SearchOption.SearchAllSubDirectories, "*.txt")

            Dim count As Integer, failcount As Integer

            For Each file As String In files

                Try

                    Logger.LoggerWriteLine("SS> Loading '" & FileIO.FileSystem.GetFileInfo(file).Name & "'")

                    tmpScript = New ServerScript(file)

                    Logger.LoggerWriteLine("SS> Parsed OK!")

                    aServerScripts.Add(tmpScript)
                    count += 1

                Catch ex As Exception

                    Output.AddLine("\B0\F9SS>\F0\B8 Script generated an error: \F7" & ex.Message & "\F0")
                    failcount += 1

                End Try

            Next

            Output.AddLine("\B0\F9SS>\F2\B8 " & count & "\F0 scripts loaded! (\F7" & failcount & " failed\F0)")

            Return failcount

        End Function

        Public Shared Function RunCommand(file As String, command As String, conn As Connection) As Integer

            For Each script As ServerScript In aServerScripts

                If Right(script.mFileName, file.Length + 1) = "\" & file Then

                    Return script.RunCommand(command, conn)

                End If

            Next

            Return 0

        End Function

        Public Shared Function SayCall(cmd As String, conn As Connection) As Integer

            Return Runcommand(Left(cmd, instr(cmd, "@") - 1), Right(cmd, cmd.Length - (instr(cmd, "@") - 1)), conn)

        End Function

    End Class

#End Region

End Namespace